package org.springframework.social.connect.support;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.springframework.core.GenericTypeResolver;
import org.springframework.social.ExpiredAuthorizationException;
import org.springframework.social.ServiceProvider;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2ServiceProvider;

public class OAuth2Connection<A> extends AbstractConnection<A> {
  private static final long serialVersionUID = 4057584084077577480L;
  
  private final transient OAuth2ServiceProvider<A> serviceProvider;
  
  private String accessToken;
  
  private String refreshToken;
  
  private Long expireTime;
  
  private transient A api;
  
  private transient A apiProxy;
  
  public OAuth2Connection(String providerId, String providerUserId, String accessToken, String refreshToken, Long expireTime, OAuth2ServiceProvider<A> serviceProvider, ApiAdapter<A> apiAdapter) {
    super(apiAdapter);
    this.serviceProvider = serviceProvider;
    initAccessTokens(accessToken, refreshToken, expireTime);
    initApi();
    initApiProxy();
    initKey(providerId, providerUserId);
  }
  
  public OAuth2Connection(ConnectionData data, OAuth2ServiceProvider<A> serviceProvider, ApiAdapter<A> apiAdapter) {
    super(data, apiAdapter);
    this.serviceProvider = serviceProvider;
    initAccessTokens(data.getAccessToken(), data.getRefreshToken(), data.getExpireTime());
    initApi();
    initApiProxy();
  }
  
  public boolean hasExpired() {
    synchronized (getMonitor()) {
      return (this.expireTime != null && System.currentTimeMillis() >= this.expireTime.longValue());
    } 
  }
  
  public void refresh() {
    synchronized (getMonitor()) {
      AccessGrant accessGrant = this.serviceProvider.getOAuthOperations().refreshAccess(this.refreshToken, null);
      initAccessTokens(accessGrant.getAccessToken(), accessGrant.getRefreshToken(), accessGrant.getExpireTime());
      initApi();
    } 
  }
  
  @Override
  public A getApi() {
    if (this.apiProxy != null)
      return this.apiProxy; 
    if (getMonitor() == null)
      return this.api; 
    synchronized (getMonitor()) {
      return this.api;
    } 
  }
  
  @Override
  public ConnectionData createData() {
    synchronized (getMonitor()) {
      return new ConnectionData(getKey().getProviderId(), getKey().getProviderUserId(), getDisplayName(), getProfileUrl(), getImageUrl(), this.accessToken, null, this.refreshToken, this.expireTime, getProviderUnionId());
    } 
  }
  
  private void initAccessTokens(String accessToken, String refreshToken, Long expireTime) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
    this.expireTime = expireTime;
  }
  
  private void initApi() {
    this.api = (A)this.serviceProvider.getApi(this.accessToken);
  }
  
  private void initApiProxy() {
    Class<?> apiType = GenericTypeResolver.resolveTypeArgument(this.serviceProvider.getClass(), ServiceProvider.class);
    if (apiType.isInterface())
      this.apiProxy = (A)Proxy.newProxyInstance(apiType.getClassLoader(), new Class[] { apiType }, new ApiInvocationHandler()); 
  }
  
  private class ApiInvocationHandler implements InvocationHandler {
    private ApiInvocationHandler() {}
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      synchronized (OAuth2Connection.this.getMonitor()) {
        if (OAuth2Connection.this.hasExpired())
          throw new ExpiredAuthorizationException(OAuth2Connection.this.getKey().getProviderId()); 
        try {
          return method.invoke(OAuth2Connection.this.api, args);
        } catch (InvocationTargetException e) {
          throw e.getTargetException();
        } 
      } 
    }
  }
  
  @Override
  public int hashCode() {
    int prime = 31;
    int result = super.hashCode();
    result = 31 * result + ((this.accessToken == null) ? 0 : this.accessToken.hashCode());
    result = 31 * result + ((this.expireTime == null) ? 0 : this.expireTime.hashCode());
    result = 31 * result + ((this.refreshToken == null) ? 0 : this.refreshToken.hashCode());
    return result;
  }
  
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true; 
    if (!super.equals(obj))
      return false; 
    if (getClass() != obj.getClass())
      return false; 
    OAuth2Connection other = (OAuth2Connection)obj;
    if (this.accessToken == null) {
      if (other.accessToken != null)
        return false; 
    } else if (!this.accessToken.equals(other.accessToken)) {
      return false;
    } 
    if (this.expireTime == null) {
      if (other.expireTime != null)
        return false; 
    } else if (!this.expireTime.equals(other.expireTime)) {
      return false;
    } 
    if (this.refreshToken == null) {
      if (other.refreshToken != null)
        return false; 
    } else if (!this.refreshToken.equals(other.refreshToken)) {
      return false;
    } 
    return true;
  }
}
